//react-router不能适应这个Relay的预先获取数据后显示页面的方式。我这里路由器实际仅用了react-router包的底层很小部分功能。无法正常跟随版本升级！
//据说react-router 6.0能搞定问题？不需要自己定做；？ ，矛盾问题大，需要定做=舍弃与选择。
//可参考D:\coding\Relay\typescript-relayjs-examples-master\relay-entrypoints\src\ServerEntry.ts
//history 4.10.1版本配套react-router的5版本，react-router6版本没有静态路由没法用。
//只能滞留history上一代版本。Version 4 is used in React Router versions 5.这通过"react-router"包间接引用的history包。
import { createBrowserHistory } from 'history';
//这个包实际关联还用了react-router包+history包。6.0版本react-router不需要react-router-config的目前还不支持。
//我这个 react-router-config 也间接用了包 'react-router' 5版本的；官方说明Use useRoutes instead of react-router-config
import { matchRoutes } from 'react-router-config';

/**
 * 定制化程度非常深：都没用到底层react-router-config包其实有的renderRoutes/新版改做renderMatches ，
 * 仅仅用matchRoutes但是新版本参数RouteObject[]的结构定义和V6.0新版都不一样；history包的话也有类似问题;
  history用来浏览器侦听=触发prepareMatches准备prepare执行的。 @结论=！无法升级react-router V6.0版。
 和react-router的发展方向岔开路了！。我这是静态配置路由器。react-router是动态路由器。
 * 定做路由器提供 代替react-router-dom包的功能，所以不能使用react-router-dom包的！
 * react-router下一代6.0版本=Concurrent/Suspense Ready ?每个大版本都不一样！ 6.0版也不支持Relay提前加载数据哦;
 * A custom router built from the same primitives as react-router. Each object in `routes`
 * contains both a Component and a prepare() function that can preload data for the component.
 * The router watches for changes to the current location via the `history` package, maps the
 * location to the corresponding route entry, and then preloads the code and data for the route.
 * 根据路由表对象routes【】来生成context的。
 * context才有用的。
 * react-router包提供的useParams;useRouteMatch;全部无法使用啊！，我这里自定义路由Context没对上？。
 * 静态路由是普通路由，多数框架的理念，动态路由却是react-router4.0以后版本新理念,无需独立的，render集成在一起的，大小屏幕参数?=>跳转。
 * 【更新】pathname如果没有变更的不会导致组件重新加载的,就算init在上级组件有变化值，那么React.useState(init),useRef(init)还会是上次的值，不会跟随上级组件init变化的。
 * 这个文件的范围：无法使用Hook，不归React管辖。
 */
export default function createRouter(routes, options) {
  // Initialize history ; 若是node.js环境就要用到对应createMemoryHistory(); 正常web是H5浏览器我这就是BrowserHistory,
  const history = createBrowserHistory(options);
  let  userInit=null;      //保存当前的用户数据
  // Find the initial match and prepare it
  const initialMatches = matchRoute(routes, history.location);
  const initialEntries = prepareMatches(null, initialMatches, userInit);
  //这个是RouteRenderer.js对接到的 render嵌套要获取的数据。
  //底下数据结构：entries实际是个[],要传递给context,路由RouterRenderer组件就依据它还原出reversedItems[]进行组件多层次的嵌套拼装的。
  let currentEntry = {
    location: history.location,
    entries: initialEntries,
  };

  // maintain a set of subscribers to the active entry
  let nextId = 0;
  const subscribers = new Map();

  // Listen for location changes, match to the route entry, prepare the entry,
  // and notify subscribers. Note that this pattern ensures that data-loading
  // occurs *outside* of - and *before* - rendering.
  /**注册 history监听路由变化的 机制
   * 页面切换的 触发关键！ location有底下几个组成的：
      pathname: ;   search: ;   state: ;   hash: ;   key?: ;
   && location.state === currentEntry.location.state
   状态.state变了，数据就变化，页面应当跟随变。
   因location.key两次点击同一个连接也变掉。这路由器却不一定认定路由变必须重新render()的。currentEntry:是当前的即将跳转掉。
   * */
  const cleanup = history.listen((location, action) => {
    const stateEq=JSON.stringify(location.state) === JSON.stringify(currentEntry.location.state);
    console.log(`路由器侦听URL:`,currentEntry.location.pathname, location);
    if (location.pathname === currentEntry.location.pathname && stateEq ) {
       //为了解决问题：轮播台左边列表点击同一个链接第二次不会切换到右边页面显示的。添加这里重新render:
        const nextEntry = {
          location,
          entries: currentEntry.entries,
        };
        currentEntry.location = location;
        //会导致组件render一次！但是组件不会remount的。组件里面没法捕捉location， useEffect依赖location不会触发？。原始记录动态加载自己定义路由的内部子组件不会触发render。
        subscribers.forEach(cb => cb(nextEntry));
        return;
    }
    else{
            //【通用标记 time: 】支持history.replace(nowUrl, {time: Date()} ) 刷新当前页面不保证能remount页面组件！
      if(location.pathname === currentEntry.location.pathname) {
        //console.log(`listen路由#变pathname没变`,currentEntry.location.pathname, location);
        //对比抄着 preload(pathname) {}
        const matches = matchRoutes(routes, location.pathname);
        //matches.forEach(({ route }) => route.component.load()); 【确保】重新render,但是不保证remount组件：会导致Input组件初始化后没法变更取值的。
        //state变动后【强制刷新】;本来应该传递currentEntry，因需要强制重新加载@变化的设置centry=null。数据刷新：但是组件当前页面path同一个的不会remount
        const entries = prepareMatches(null, matches, userInit);
        const nextEntry = {
          location,
          entries,
        };
        currentEntry = nextEntry;
        //没变currentEntry = nextEntry; 若href没改 + 但是location.state可能修改了。
        //运行cb()这里，才能导致页面内容的更新:
        subscribers.forEach(cb => cb(nextEntry));
        return;
      }
    }

    const matches = matchRoute(routes, location);
    //some() 方法会依次执行数组的每个元素：如果有一个元素满足条件，则表达式返回true , 剩余的元素不会再执行检测。
    //实际上只能返回一个路由的;
    const entries = prepareMatches(currentEntry, matches, userInit);
    const nextEntry = {
      location,
      entries,
    };
    currentEntry = nextEntry;
    subscribers.forEach(cb => cb(nextEntry));
  });

  //传给<RoutingContext.Provider value= 没有类型定义？
  // The actual object that will be passed on the RoutingConext.
  //底下history等5个参数项目都是有用的。提供给<Link>使用预加载源代码。
  //【核心点】初始化 APP的 RouterContext RoutingContext
  const context = {
    history,
    get() {
      return currentEntry;
    },
    preloadCode(pathname, user) {
      if(userInit!==user)
        userInit=user;
      //根据URI来加载组件代码，但是不会主动获取数据。
      // preload just the code for a route, without storing the result
      const matches = matchRoutes(routes, pathname);
      //预加载的情况: 点击Link 链接或者 浏览器地址栏目输入URI进入的;  #组件loadable__component它提供这个component.preload()函数
      matches.forEach(({ route }) => route.component.preload());
      //非预加载的模式:
      //matches.forEach(({ route }) => route.component);
    },
    preload(pathname, user) {
      // preload the code and data for a route, without storing the result
      //点击路由赋能的Link链接，会提前加载数据。
      const matches = matchRoutes(routes, pathname);
      if(userInit!==user)
        userInit=user;
      prepareMatches(currentEntry, matches, user);
    },
    subscribe(cb) {
      const id = nextId++;
      const dispose = () => {
        subscribers.delete(id);
          //timer取消吗:RouterRenderer;
      };
      //导致页面内容切换的入口是这里：cb()回调函数去执行cb:
      subscribers.set(id, cb);
      return dispose;
    },
  };

  // Return both the context object and a cleanup function
  return { cleanup, context };
}

/** 把URL转为路由配置的多层次分解路由数组
 * Match the current location to the corresponding route entry.
 */
function matchRoute(routes, location) {
  const matchedRoutes = matchRoutes(routes, location.pathname);
  if (!Array.isArray(matchedRoutes) || matchedRoutes.length === 0) {
    throw new Error('No route for ' + location.pathname);
  }
  return matchedRoutes;
}

/**核心准备，返回是entries[]新数组：实际是准备进入页面的数据以及准备展示的各组件。 触发加载数据和代码。
 * Load the data for the matched route, given the params extracted from the route
 * 如果是静态的组件 也会延迟源程序代码加载但是无数据的；
 * 注意 route.component是组件源代码，并非组件本尊。
 * 实际上点击才触发的，浏览器地址URL输入触发。
 * 准备每一个层次路由匹配到的组件以及为组件加载数据
 * 【定做】关联的user; 直接注入user,否则只能依赖URL传参数 或者 是客户端存储保存读取来恢复参数了,这个办法跟通用点。user代表客户身份相关的json对象。
 * 这个参数centry是{location: history.location, entries: initialEntries,}类型对象：实际是当前页面路由和数据以及已经加载的组件存储。
 * centry实际就是路由切换之前的历史页面组件状态。
 * 奇怪，若secondRouter([{
 *               path: '/zeebeTodo',
 *               exact: true,    //若不设置true:导致后面多个二级路由层次的情况配置的'/inspect'开头的路由失效！
 *               component:
 *               },
 *               routes:[ { }, ],
 *           },
 *           ]这样配置，导致底下的matches.map((match, lev)  => {})有3个level:不是预期的2层啊。
 *底下的centry?.entries[lev].routeData亦即matches[i].match数据类型是： history库给的：真的没有牵涉?&parm=val或#hashx其它的？没法区分？需强制更新?;例子如下的：
 * {
 *  isExact: true
 *  params: {rqId: 'kA5rhD1tR8umSxZJ8Xqh9lJlcXVlc3Q'}
 *  path: "/request/:rqId"
 *  url: "/request/kA5rhD1tR8umSxZJ8Xqh9lJlcXVlc3Q"
 * }
 * 申请单流转给本人，申请单数据没法立刻保持一致显示，流程引擎实际滞后才去更新数据库，没法立刻跟随点击触发函数反馈前端：困扰：每次进入页面都要从后端提取实体？先确保读取新数据后push()做render；
 * 【解决】最后依靠useMyMutation(){ updater: {request.invalidateRecord();来主动把数据拉成失效，下次进入需要该数据的组件自动重新获取来解决问题，从路由器方面改的不好！不能确保能remount;
 */
function prepareMatches(centry, matches, user) {
  //每一个路由匹配到的组件都会加载数据。 常见路由页面matches是3层的[3]数组; 若是前面1层2层相同的就没必要重新执行prepare去加载数据了。
  //console.log("prepareMatches旧entry=", centry,"新match=",matches);
  const oldLevelCnt= centry?.entries?.length;

  return matches.map((match, lev)  => {
    const { route, match: matchData } = match;
    //避免出现没必要重新mount的组件的遗留数据状态的丢失！保留旧的数据列表。但是路由完全变动再次转回来的基础组件会被卸载了只能走重新mount组件和再次执行prepare()的路径，旧数据状态无法自动还原。
    if(lev < oldLevelCnt){
      const old= centry?.entries[lev].routeData;        //当前页面遗留entry路由匹配。
      if(old.url===matchData.url && old.path===matchData.path && JSON.stringify(old.params)===JSON.stringify(matchData.params) ) {
        //避免重复更新 免重新执行路由的prepare() 最高0层AuthComp除外:规避登录失效。 遇到matchData.isExact=true的切换#
        if(lev!==0 || old.path!=='/') {
            return {
                component: centry?.entries[lev].component,
                prepared: centry?.entries[lev].prepared,
                routeData: matchData
            };
        }
      }
    }

    //真正发起服务端请求的。
    //点击一次，可能执行两次的route.prepare(matchData.params);
    //【二级路由】想要注入上一层的参数，上级路由传递user给下一级路由函数的情形。
    //prepare函数()=>{}入口参数只能是：matchData.params限制死了吗？ 【定做】路由表prepare函数同样增加一个参数user提供当前用户。路由函数内无法Hook提取user;
    const prepared= route.prepare? route.prepare(matchData.params, user) : null;
    //prepared？还没有获得数据：最终要传递给DeviceMain这样的入口组件去。
    //console.log("prepareMatches跑函prepared=", prepared,"matchData=",matchData);
    //.get()返回可为null
    //const Component = route.component.get();
    //eagerly load 加载源程序代码；
    if(route.component) {
      route.component.preload();    //触发加载源代码：【不是一个东西】preload是第三方组件loadable__component它提供这个函数！
    }
    //prepared= 加载Relay数据的请求/数据提前获取。
    //【特别小心！】这个地方不能指望直接能component.read到组件代码，否则promise抛出错误！！
    //var _resource$read = route.component.read();
    //只能是单一个文件名的缺省输出的！
    //const  Component = _resource$read["default"];
    //const Component = props.component.read();
    //console.log("prepareMatches跑来route.component=", route.component,"matchData=",matchData);
    //route.component实际上还不是组件的
    return { component: route.component, prepared, routeData: matchData };
  });

  /* 想要直接能component.read到组件代码，
    promises= matches.map(match => {return { component: route.component, prepared, routeData: matchData };
               });
    return Promise.all(promises);
  **/
}


//已经封装已成了,不能直接用window.history.pushState(stateObj, "","/device")搞什么状态取值。